/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4graph;

import java.awt.Color;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import static java.lang.StrictMath.PI;
import static java.lang.StrictMath.sin;
import static java.lang.StrictMath.cos;
import static java.lang.StrictMath.atan2;
import static java.lang.StrictMath.toRadians;
import static edu.mit.csail.sdg.alloy4graph.Artist.getBounds;
import static edu.mit.csail.sdg.alloy4graph.Graph.selfLoopA;
import static edu.mit.csail.sdg.alloy4graph.Graph.selfLoopGL;
import static edu.mit.csail.sdg.alloy4graph.Graph.selfLoopGR;
import static edu.mit.csail.sdg.alloy4graph.Graph.esc;

/** Mutable; represents a graphical edge.
 *
 * <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
 */

public final strictfp class GraphEdge {

   // =============================== adjustable options ===========================================================================

   /** The angle (in radian) to fan out the arrow head, if the line is not bold. */
   private final double smallFan = toRadians(16);

   /** The angle (in radian) to fan out the arrow head, if the line is bold. */
   private final double bigFan = toRadians(32);

   // =============================== cached for performance efficiency ============================================================

   /** The maximum ascent and descent. We deliberately do NOT make this field "static" because only AWT thread can call Artist. */
   private final int ad = Artist.getMaxAscentAndDescent();

   // =============================== fields =======================================================================================

   /** a user-provided annotation that will be associated with this edge (can be null) (need not be unique) */
   public final Object uuid;

   /** a user-provided annotation that will be associated with this edge (all edges with same group will be highlighted together) */
   public final Object group;

   /** The "from" node;  must stay in sync with GraphNode.ins and GraphNode.outs and GraphNode.selfs */
   private GraphNode a;

   /** The "to" node;  must stay in sync with GraphNode.ins and GraphNode.outs and GraphNode.selfs */
   private GraphNode b;

   /** The label (can be "");  NOTE: label will be drawn only if the start node is not a dummy node. */
   private final String label;

   /** Whether to draw an arrow head on the "from" node; default is false. */
   private boolean ahead = false;

   /** Whether to draw an arrow head on the "to" node; default is true. */
   private boolean bhead = true;

   /** The color of the edge; default is BLACK; never null. */
   private Color color = Color.BLACK;

   /** The line-style of the edge; default is SOLID; never null. */
   private DotStyle style = DotStyle.SOLID;

   /** The edge weight; default is 1; always between 1 and 10000 inclusively. */
   private int weight = 1;

   /** The location and size of the label box; initially (0, 0, label.width, label.height) */
   private final AvailableSpace.Box labelbox;

   /** The actual path corresponding to this edge; initially null until it's computed. */
   private Curve path = null;

   // =========================================================================s====================================================

   /** Construct an edge from "from" to "to" with the given arrow head settings, then add the edge to the graph. */
   GraphEdge(GraphNode from, GraphNode to, Object uuid, String label, boolean drawArrowHeadOnFrom, boolean drawArrowHeadOnTo, DotStyle style, Color color, Object group) {
      if (group instanceof GraphNode) throw new IllegalArgumentException("group cannot be a GraphNode");
      if (group instanceof GraphEdge) throw new IllegalArgumentException("group cannot be a GraphEdge");
      if (group == null) { group = new Object(); }
      a = from;
      b = to;
      if (a.graph != b.graph) throw new IllegalArgumentException("You cannot draw an edge between two different graphs.");
      if (a == b) { a.selfs.add(this); } else { a.outs.add(this); b.ins.add(this); }
      a.graph.edgelist.add(this);
      this.uuid = uuid;
      this.group = group;
      this.label = (label==null) ? "" : label;
      this.ahead = drawArrowHeadOnFrom;
      this.bhead = drawArrowHeadOnTo;
      if (style!=null) this.style = style;
      if (color!=null) this.color = color;
      if (this.label.length()>0) {
         Rectangle2D box = getBounds(false, label);
         labelbox = new AvailableSpace.Box(0, 0, (int) box.getWidth(), (int) box.getHeight());
      } else {
         labelbox = new AvailableSpace.Box(0, 0, 0, 0);
      }
   }

   /** Construct an edge from "from" to "to", then add the edge to the graph. */
   public GraphEdge(GraphNode from, GraphNode to, Object uuid, String label, Object group) {
      this(from, to, uuid, label, false, true, null, null, group);
   }

   /** Returns the "from" node. */
   public GraphNode a() { return a; }

   /** Returns the "to" node. */
   public GraphNode b() { return b; }

   /** Swaps the "from" node and "to" node. */
   void reverse() {
      if (a == b) return;
      a.outs.remove(this);
      b.ins.remove(this);
      a.ins.add(this);
      b.outs.add(this);
      GraphNode x=a; a=b; b=x;
   }

   /** Changes the "to" node to the given node. */
   void change(GraphNode newTo) {
      if (b.graph != newTo.graph) throw new IllegalArgumentException("You cannot draw an edge between two different graphs.");
      if (a==b) a.selfs.remove(this); else { a.outs.remove(this); b.ins.remove(this); }
      b = newTo;
      if (a==b) a.selfs.add(this); else { a.outs.add(this); b.ins.add(this); }
   }

   /** Return the X coordinate of the top-left corner of the label box. */
   public int getLabelX() { return labelbox.x; }

   /** Return the Y coordinate of the top-left corner of the label box. */
   public int getLabelY() { return labelbox.y; }

   /** Return the width of the label box. */
   public int getLabelW() { return labelbox.w; }

   /** Return the height of the label box. */
   public int getLabelH() { return labelbox.h; }

   /** Returns the edge weight (which is always between 1 and 10000 inclusively). */
   public int weight() { return weight; }

   /** Returns the line style; never null. */
   public DotStyle style() { return style; }

   /** Returns the line color; never null. */
   public Color color() { return color; }

   /** Returns true if we will draw an arrow head on the "from" node. */
   public boolean ahead() { return ahead; }

   /** Returns true if we will draw an arrow head on the "to" node. */
   public boolean bhead() { return bhead; }

   /** Returns the label on this edge. */
   public String label() { return label; }

   /** Sets the edge weight between 1 and 10000. */
   public GraphEdge set(int weightBetween1And10000) {
      if (weightBetween1And10000 < 1) weightBetween1And10000 = 1;
      if (weightBetween1And10000 > 10000) weightBetween1And10000 = 10000;
      weight = weightBetween1And10000;
      return this;
   }

   /** Sets whether we will draw an arrow head on the "from" node, and whether we will draw an arrow head on the "to" node. */
   public GraphEdge set(boolean from, boolean to) {
      this.ahead = from;
      this.bhead = to;
      return this;
   }

   /** Sets the line style. */
   public GraphEdge set(DotStyle style) {
      if (style != null) this.style = style;
      return this;
   }

   /** Sets the line color. */
   public GraphEdge set(Color color) {
      if (color != null) this.color = color;
      return this;
   }

   /** Returns the current path; if the path was not yet assigned, it returns a straight line from "from" node to "to" node. */
   Curve path() {
      if (path==null) resetPath();
      return path;
   }

   /** Reset the path as a straightline from the center of the "from" node to the center of the "to" node. */
   void resetPath() {
      double ax = a.x(), ay = a.y();
      if (a==b) {
         double w = 0;
         for(int n = a.selfs.size(), i = 0; i < n; i++) {
            if (i==0) w = a.getWidth()/2 + selfLoopA;
                 else w = w + getBounds(false, a.selfs.get(i-1).label()).getWidth() + selfLoopGL + selfLoopGR;
            GraphEdge e = a.selfs.get(i);
            if (e!=this) continue;
            double h=a.getHeight()/2D*0.7D, k=0.55238D, wa=(a.getWidth()/2.0D), wb=w-wa;
            e.path = new Curve(ax, ay);
            e.path.cubicTo(ax, ay-k*h, ax+wa-k*wa, ay-h, ax+wa, ay-h);
            e.path.cubicTo(ax+wa+k*wb, ay-h, ax+wa+wb, ay-k*h, ax+wa+wb, ay);
            e.path.cubicTo(ax+wa+wb, ay+k*h, ax+wa+k*wb, ay+h, ax+wa, ay+h);
            e.path.cubicTo(ax+wa-k*wa, ay+h, ax, ay+k*h, ax, ay);
            e.labelbox.x = (int) (ax + w + selfLoopGL);
            e.labelbox.y = (int) (ay - getBounds(false, e.label()).getHeight()/2);
            break;
         }
      } else {
         int i=0, n=0;
         for(GraphEdge e: a.outs) { if (e==this) i=n++; else if (e.b==b) n++; }
         double cx=b.x(), cy=b.y(), bx=(ax+cx)/2, by=(ay+cy)/2;
         path = new Curve(ax, ay);
         if (n>1 && (n&1)==1) {
            if (i<n/2) bx = bx - (n/2-i)*10; else if (i>n/2) bx = bx + (i-n/2)*10;
            path.lineTo(bx, by).lineTo(cx, cy);
         } else if (n>1) {
            if (i<n/2) bx = bx - (n/2-i)*10 + 5; else bx = bx + (i-n/2)*10 + 5;
            path.lineTo(bx, by).lineTo(cx, cy);
         } else {
            path.lineTo(cx, cy);
         }
      }
   }

   /** Given that this edge is already well-laid-out, this method moves the label hoping to avoid/minimize overlap. */
   void repositionLabel(AvailableSpace sp) {
      if (label.length()==0 || a==b) return; // labels on self-edges are already re-positioned by GraphEdge.resetPath()
      final int gap = style==DotStyle.BOLD ? 4 : 2; // If the line is bold, we need to shift the label to the right a little bit
      boolean failed = false;
      Curve p = path;
      for(GraphNode a = this.a; a.shape()==null;) { GraphEdge e = a.ins.get(0);  a = e.a; p = e.path().join(p); }
      for(GraphNode b = this.b; b.shape()==null;) { GraphEdge e = b.outs.get(0); b = e.b; p = p.join(e.path()); }
      for(double t=0.5D; ; t=t+0.05D) {
         if (t>=1D) { failed=true; t=0.7D; }
         double x1 = p.getX(t), y = p.getY(t), x2 = p.getXatY(y+labelbox.h, t, 1D, x1);
         int x = (int) (x1<x2 ? x2+gap : x1+gap);
         if (failed || sp.ok(x, (int)y, labelbox.w, labelbox.h)) { sp.add(labelbox.x=x, labelbox.y=(int)y, labelbox.w, labelbox.h); return; }
         double t2=1D-t;
         x1 = p.getX(t2); y = p.getY(t2); x2 = p.getXatY(y+labelbox.h, t2, 1D, x1);
         x = (int) (x1<x2 ? x2+gap : x1+gap);
         if (sp.ok(x, (int)y, labelbox.w, labelbox.h)) { sp.add(labelbox.x=x, labelbox.y=(int)y, labelbox.w, labelbox.h); return; }
      }
   }

   /** Positions the arrow heads of the given edge properly. */
   void layout_arrowHead() {
      Curve c=path();
      if (ahead() && a.shape()!=null) {
         double in=0D, out=1D;
         while(StrictMath.abs(out-in)>0.0001D) {
            double t=(in+out)/2;
            if (a.contains(c.getX(t), c.getY(t))) in=t; else out=t;
         }
         c.chopStart(in);
      }
      if (bhead() && b.shape()!=null) {
         double in=1D, out=(a==b ? 0.5D : 0D);
         while(StrictMath.abs(out-in)>0.0001D) {
            double t=(in+out)/2;
            if (b.contains(c.getX(t), c.getY(t))) in=t; else out=t;
         }
         c.chopEnd(in);
      }
   }

   /** Assuming this edge's coordinates have been properly assigned, and given the current zoom scale, draw the edge. */
   void draw(Artist gr, double scale, GraphEdge highEdge, Object highGroup) {
      final int top = a.graph.getTop(), left = a.graph.getLeft();
      gr.translate(-left, -top);
      if (highEdge==this) { gr.setColor(color); gr.set(DotStyle.BOLD, scale); }
      else if ((highEdge==null && highGroup==null) || highGroup==group) { gr.setColor(color); gr.set(style, scale); }
      else { gr.setColor(Color.LIGHT_GRAY); gr.set(style, scale); }
      if (a == b) {
         gr.draw(path);
      } else {
         // Concatenate this path and its connected segments into a single VizPath object, then draw it
         Curve p=null;
         GraphEdge e=this;
         while(e.a.shape() == null) e = e.a.ins.get(0); // Let e be the first segment of this chain of connected segments
         while(true) {
            p = (p==null) ? e.path : p.join(e.path);
            if (e.b.shape()!=null) break;
            e = e.b.outs.get(0);
         }
         gr.drawSmoothly(p);
      }
      gr.set(DotStyle.SOLID, scale);
      gr.translate(left, top);
      if (highEdge==null && highGroup==null && label.length()>0) drawLabel(gr, color, null);
      drawArrowhead(gr, scale, highEdge, highGroup);
   }

   /** Assuming this edge's coordinates have been properly assigned, and given the desired color, draw the edge label. */
   void drawLabel(Artist gr, Color color, Color erase) {
      if (label.length()>0) {
         final int top = a.graph.getTop(), left = a.graph.getLeft();
         gr.translate(-left, -top);
         if (erase!=null && a!=b) {
            Rectangle2D.Double rect = new Rectangle2D.Double(labelbox.x, labelbox.y, labelbox.w, labelbox.h);
            gr.setColor(erase);
            gr.draw(rect, true);
         }
         gr.setColor(color);
         gr.drawString(label, labelbox.x, labelbox.y+Artist.getMaxAscent());
         gr.translate(left, top);
         return;
      }
   }

   /** Assuming this edge's coordinates have been assigned, and given the current zoom scale, draw the arrow heads. */
   private void drawArrowhead(Artist gr, double scale, GraphEdge highEdge, Object highGroup) {
      final double tipLength = ad * 0.6D;
      final int top = a.graph.getTop(), left = a.graph.getLeft();
      // Check to see if this edge is highlighted or not
      double fan = (style==DotStyle.BOLD ? bigFan : smallFan);
      if (highEdge==this) { fan=bigFan; gr.setColor(color); gr.set(DotStyle.BOLD, scale); }
      else if ((highEdge==null && highGroup==null) || highGroup==group) { gr.setColor(color); gr.set(style, scale); }
      else { gr.setColor(Color.LIGHT_GRAY); gr.set(style, scale); }
      for(GraphEdge e = this ; ; e = e.b.outs.get(0)) {
         if ((e.ahead && e.a.shape()!=null) || (e.bhead && e.b.shape()!=null)) {
            Curve cv=e.path();
            if (e.ahead && e.a.shape()!=null) {
               CubicCurve2D.Double bez = cv.list.get(0);
               double ax = bez.x1, ay = bez.y1, bx = bez.ctrlx1, by = bez.ctrly1;
               double t = PI + atan2(ay-by, ax-bx);
               double gx1 = ax + tipLength*cos(t-fan), gy1 = ay + tipLength*sin(t-fan);
               double gx2 = ax + tipLength*cos(t+fan), gy2 = ay + tipLength*sin(t+fan);
               GeneralPath gp=new GeneralPath();
               gp.moveTo((float)(gx1-left), (float)(gy1-top)); gp.lineTo((float)(ax-left), (float)(ay-top));
               gp.lineTo((float)(gx2-left), (float)(gy2-top)); gp.closePath(); gr.draw(gp,true);
            }
            if (e.bhead && e.b.shape()!=null) {
               CubicCurve2D.Double bez = cv.list.get(cv.list.size()-1);
               double bx = bez.x2, by = bez.y2, ax = bez.ctrlx2, ay = bez.ctrly2;
               double t = PI + atan2(by-ay, bx-ax);
               double gx1 = bx + tipLength*cos(t-fan), gy1 = by + tipLength*sin(t-fan);
               double gx2 = bx + tipLength*cos(t+fan), gy2 = by + tipLength*sin(t+fan);
               GeneralPath gp=new GeneralPath();
               gp.moveTo((float)(gx1-left), (float)(gy1-top)); gp.lineTo((float)(bx-left), (float)(by-top));
               gp.lineTo((float)(gx2-left), (float)(gy2-top)); gp.closePath(); gr.draw(gp,true);
            }
         }
         if (e.b.shape()!=null) break;
      }
   }

   /** Returns a DOT representation of this edge (or "" if the start node is a dummy node) */
   @Override public String toString() {
      GraphNode a = this.a, b = this.b;
      if (a.shape() == null) return ""; // This means this edge is virtual
      while(b.shape() == null) { b = b.outs.get(0).b; }
      String color = Integer.toHexString(this.color.getRGB() & 0xFFFFFF);  while(color.length() < 6) { color = "0" + color; }
      StringBuilder out = new StringBuilder();
      out.append("\"N" + a.pos() + "\"");
      out.append(" -> ");
      out.append("\"N" + b.pos() + "\"");
      out.append(" [");
      out.append("uuid = \"" + (uuid==null ? "" : esc(uuid.toString())) + "\"");
      out.append(", color = \"#" + color + "\"");
      out.append(", fontcolor = \"#" + color + "\"");
      out.append(", style = \"" + style.getDotText() + "\"");
      out.append(", label = \"" + esc(label) + "\"");
      out.append(", dir = \"" + (ahead && bhead ? "both" : (bhead ? "forward" : "back")) + "\"");
      out.append(", weight = \"" + weight + "\"");
      out.append("]\n");
      return out.toString();
   }
}
